Ce notebook est une version de synthèse du notebook de travail originel.
La société financière "Prêt à dépenser" souhaite pouvoir utiliser un modèle de scoring l'aidant à prédire le risque de défaut de paiement d'un client ayant peu ou pas d'historique de prêt.
Le modèle devra permettre aux conseillers qui l'utilisent de comprendre les motifs de l'acceptation ou du rejet de la demande de prêt.
import pandas as pd
pd.set_option('display.max_colwidth', 200, 'display.max_rows', None, 'display.max_columns', None)
import numpy as np
from tqdm import tqdm
import plotly.express as px
import plotly.offline as py
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import make_column_transformer
from sklearn.pipeline import make_pipeline
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, GridSearchCV, RandomizedSearchCV
from sklearn.metrics import confusion_matrix, plot_confusion_matrix, fbeta_score, make_scorer, accuracy_score
from sklearn.dummy import DummyClassifier
print('Imports terminés.')
Imports terminés.
dirpath = 'files/'
application_train = pd.read_csv(dirpath + 'application_train.csv')
Sur les 9 fichiers fournis (hors celui détaillant les informations contenues dans les autres) :
Puisque le modèle est destiné à évaluer le risque de défaut d'emprunteurs n'ayant pas d'historique de crédit, il est bien sûr à utiliser.
application_test.csv ne nous sera pas utile dans le présent cas, car il ne comporte pas d'étiquettes relatives au sort du prêt (défaut ou pas). On ne l'utilisera donc pas.
POS_CASH_balance.csv : ce fichier est relatif au prêt en cours et au prêt précédent. Les données ne seront donc pas disponibles pour un nouvel emprunteur : il n'est donc pas utile pour l'entraînement du modèle.
bureau.csv : contient des informations sur le prêt en cours, donc là encore, non utilisable vu notre objectif.
bureau_balance.csv : La description précise "use this to join to CREDIT_BUREAU table", qui n'apparaît pas dans la liste. On prendra donc l'hypothèse qu'il faut la joindre avec la table bureau. Cette dernière n'étant pas utilisée, la présente table ne sera pas non plus utile.
credit_card_balance : se réfère au prêt en cours et au prêt précédent, donc inutile
installment_payments : idem
previous_application_csv: idem
sample_submission.csv : la signification de ce ficher n'est pas évidente puisque toutes les valeurs TARGET sont à 0.5. Faute d'information sur son utilité, il ne sera pas utilisé.
Au final, seules les données contenues dans application_train.csv seront utilisées pour l'entraînement du modèle.
On définit une classe Cleaner avec deux méthodes :
clean_outliers() pour traiter les lignes : valeurs aberrantes, manquantes, valeurs à reformater
clean_features() pour traiter les colonnes : suppression des variables redondantes, création de variables synthétiques
class Cleaner:
def __init__(self):
pass
def _z_score(self, array, threshold):
'''Return an array of boolean, True for each value in the array where Z-score >threshold.'''
mean = array.mean()
std = array.std()
return abs((array-mean))/std > threshold
def clean_outliers(self, df):
'''
Return a copy of df modified :
- dropped lines with outlier values (z-score>3)
- corrected date values(values > 0 --> =0, values < DAYS_BIRTH --> =DAYS_BIRTH)
- convert DAYS values to YEARS values (dividing by -365) and rename them
'''
print('Nettoyage des lignes...')
old_length = df.shape[0]
result = df.copy()
num_cols = [col for col in result.columns if result[col].dtype!='object']
cat_cols = [col for col in result.columns if col not in num_cols]
# Drop lines where z-score > 3
high_z_score = result[num_cols].apply(self._z_score, axis=0, args=[80])
result.drop(high_z_score[high_z_score.any(axis=1)].index, inplace=True)
# Limit day values to DAYS_BIRTH, exclude outliers, convert to positive years
days_cols = [col for col in result.columns if "DAYS_" in col]
for col in days_cols:
result.loc[result[col]< result['DAYS_BIRTH'], col] = result['DAYS_BIRTH']
result.loc[result[col]>0, col] = 0
result[days_cols] = result[days_cols]/-365
# Replace NA values according to type
result[num_cols]=result[num_cols].fillna(0)
result[cat_cols]=result[cat_cols].fillna('Not specified')
# rename DAY columns
years_cols = [name.replace('DAYS', 'YEARS') for name in days_cols]
columns_dict = dict(zip(days_cols, years_cols))
result = result.rename(columns=columns_dict)
# Display summary
new_length = result.shape[0]
deleted_rows = old_length - new_length
print(f'{deleted_rows} lignes ont été supprimées soit {deleted_rows/old_length:.2%} du total.')
result.reset_index(inplace=True)
return result
def clean_features(self, df):
'''Return a copy of df after dropping useless features and creating synthetic features.'''
pd.options.mode.chained_assignment = None
print('Nettoyage des colonnes...')
# Drop columns with a single value
corr = df.corr()
constant_cols = corr[corr['AMT_INCOME_TOTAL'].isna()].index.to_list()
df.drop(constant_cols, axis=1, inplace=True)
# Drop MODE and MEDI columns
cols_without_mode_and_medi = [col for col in df.columns
if not (col.endswith('_MODE') or col.endswith('_MEDI'))]
df = df[cols_without_mode_and_medi]
# Create new synthetic features
df['CREDIT_INCOME_RATIO'] = df['AMT_CREDIT'] / df['AMT_INCOME_TOTAL']
df['CREDIT_GOOD_RATIO'] = df['AMT_CREDIT']/(df['AMT_GOODS_PRICE']+1)
df['FLOORS_AVG'] = (df['FLOORSMAX_AVG'] + df['FLOORSMIN_AVG'])/2
# Drop redundant columns
cols_to_drop = ['FLAG_EMP_PHONE', 'OBS_60_CNT_SOCIAL_CIRCLE', 'DEF_60_CNT_SOCIAL_CIRCLE',
'AMT_ANNUITY','AMT_CREDIT', 'AMT_GOODS_PRICE', 'REGION_RATING_CLIENT_W_CITY',
'ELEVATORS_AVG', 'LIVINGAPARTMENTS_AVG','LIVINGAREA_AVG','CNT_FAM_MEMBERS',
'REG_REGION_NOT_WORK_REGION', 'REG_CITY_NOT_WORK_CITY',
'FLOORSMAX_AVG','FLOORSMIN_AVG',
'ENTRANCES_AVG', 'BASEMENTAREA_AVG', 'FLOORS_AVG']
df = df.drop(cols_to_drop, axis=1)
return df
cleaner = Cleaner()
data = cleaner.clean_features(cleaner.clean_outliers(application_train))
features_cols = [col for col in data.columns if col not in ['index','TARGET', 'SK_ID_CURR']]
X = data[features_cols]
y = data['TARGET']
X.shape, y.shape
Nettoyage des lignes... 53 lignes ont été supprimées soit 0.02% du total. Nettoyage des colonnes...
((307458, 67), (307458,))
On a ramené le nombre de variables de 120 à 67.
Nous cherchons à établir un modèle de classification binaire. Le résultat peut donc être positif (cas du client qui est en défaut de paiement) ou négatif (le client rembourse normalement).
Un faux positif est un emprunteur qui serait à tort estimé comme présentant un risque de défaut.
Un faux négatif est un emprunteur qui serait estimé à tort comme ne présentant pas de risque de défaut.
L'objectif de l'établissement bancaire est de détecter le plus possible des cas positifs (sensibilité élevée), et aussi de minimiser le taux de faux négatifs : en effet, cela voudrait dire qu'elle accorde un prêt à un client qui ne le remboursera pas.
Pour autant, le taux de faux positifs n'est pas à négliger non plus car il représente pour un établissement une perte d'opportunité, en la personne d'un emprunteur qui aurait été solvable et auquel il aurait été possible de vendre d'autres produits et services.
Le taux de faux positifs est estimé par la précision (nombre total de vrais positifs divisé par le nombre total de positifs prédits). Le taux de faux négatifs est estimé par 1 - recall (le recall, ou sensibilité, étant égal au nombre de vrais positifs rapporté au nombre total de cas positifs).
L'optimisation entre la précision et le recall est donnée par la la maximisation du F1-score. Il s'agit de la moyenne harmonique de ces deux valeurs. Toutefois ce score attribue le même poids à ces derniers. Or nous souhaitons quant à nous privilégier la limitation des faux négatifs.
On utilisera donc le F2-score (c'est-à-dire un F-beta avec un Beta égal à 2), qui surpondère les faux négatifs (source) :
$ F_{\beta}=\frac{(1+{\beta}^{2})TP}{(1+{\beta}^{2})TP+{\beta}^{2}FN+FP} $
On établit un dummy classifier qui servira de point de comparaison pour évaluer l'efficacité du modèle. Pour le choix du type de dummy classifier, vérifions la répartition des classes dans les données :
print("Proportion de cas positifs dans le jeu de données : ", "{:.2%}".format(sum(y)/len(y)))
Proportion de cas positifs dans le jeu de données : 8.07%
Comme on pouvait s'y attendre, les cas de défaut sont très minoritaires. Un modèle qui prédirait systématiquement la classe négative (pas de défaut de paiement, l'emprunteur paie normalement) aurait donc une accuracy de l'ordre de 92%.
On choisit un Dummy Classifier qui renvoie des prédictions avec la même distribution que le jeu d'entraînement, afin d'avoir la même sensibilité (taux de vrais positifs) et la même spécificité (taux de vrais négatifs).
dum = DummyClassifier(strategy='stratified')
_ = dum.fit(X, y)
L'accuracy de ce classifieur est de :
acc_dum = dum.score(X, y)
acc_dum
0.8517098270332858
Et son F2 score est de :
y_pred_dum = dum.predict(X)
f2_dum = fbeta_score(y, y_pred_dum, beta=2)
f2_dum
0.08177739180778695
Notre modèle doit donc surperformer ce classifieur naïf.
La performance sera évaluée au regard du F2-score, car la précision n'est pas une métrique pertinente dans le cas de données fortement déséquilibrée. Toutefois la précision sera conservée dans la suite de l'étude à titre d'information.
La classe Trainer ci-dessous va permettre de tester plusieurs modèles et d'enregistrer les performances de chacun d'eux. Pour des raisons pratique, l'entraînement ne portera pas sur l'intégralité des données mais sur une proportion d'entre elles (par défaut 5%).
class Trainer:
def __init__(self):
self.recap = pd.DataFrame(columns = ['accuracy', 'F2-score'])
self.trf_cols = []
def _transformer(self, X):
'''
Return an transformed version of X:
- standardized quantitative features
- one-hot-encoded qualitative features.
'''
num_cols = [col for col in X.columns if X[col].dtype != 'object']
cat_cols = [col for col in X.columns if col not in num_cols]
trf = make_column_transformer(
(StandardScaler(), num_cols),
(OneHotEncoder(handle_unknown='ignore'), cat_cols)
)
trf.fit(X)
self.trf_cols = num_cols + list(trf.named_transformers_['onehotencoder'].get_feature_names())
return trf
def best_estimator_random_search_cv(self, estimator, estim_params, X, y, proportion=0.05):
'''
Return fitted estimator from RandomizedSearchCV with estim_params parameters,
on proportion of X and y.
'''
transformer = self._transformer(X)
X_part, _, y_part, _ = train_test_split(X, y, train_size=proportion, stratify=y)
rand_search_cv_estimator = RandomizedSearchCV(
make_pipeline(transformer, estimator),
param_distributions=estim_params,
cv=5,
scoring={'ftwo': make_scorer(fbeta_score, beta=2), 'acc': make_scorer(accuracy_score)},
refit='ftwo',
verbose=2,
n_jobs=4,
pre_dispatch='2*n_jobs'
)
rand_search_cv_estimator.fit(X_part,y_part)
return rand_search_cv_estimator
def update_recap(self,index, estimator=None, data=None):
'''Add line to recap with accuracy and f2 score for given estimator.'''
if index in self.recap.index:
print("valeur déjà dans le récapitulatif")
elif data:
self.recap.loc[index] = data
else:
best_index = estimator.best_index_
acc = estimator.cv_results_['mean_test_acc'][best_index]
f2 = estimator.cv_results_['mean_test_ftwo'][best_index]
self.recap.loc[index] = [acc, f2]
return self.recap.style.format('{:.2%}')
Construisons un tableau récapitulatif pour suivre l'évolution des scores selon les progrès de la modélisation.
tr = Trainer()
tr.update_recap('dummy', data=(acc_dum, f2_dum))
| accuracy | F2-score | |
|---|---|---|
| dummy | 85.17% | 8.18% |
Pour pouvoir utiliser une stratégie d'optimisation KStratifiedDFolds, on utilisera la fonction RandomizedSearchCV de sckit-learn. Comme la GridSearchCV, elle permet :
Le recours à RandomizedSearchCV permet quant à lui de raccourcir le temps d'entraînement, par sélection des jeux d'hyperparamètres.
On étudie l'impact des paramètres max_depth, min_samples_split et min_samples_leaf sur le modèle.
params = {'decisiontreeclassifier__max_depth': [3, 4, 5, 8, None],
'decisiontreeclassifier__min_samples_split': [2, 4, 8, 16],
'decisiontreeclassifier__min_samples_leaf':[1, 10, 100]}
dtc_hp = tr.best_estimator_random_search_cv(DecisionTreeClassifier(), params, X, y)
Fitting 5 folds for each of 10 candidates, totalling 50 fits
plt.figure(figsize=(20,16))
_=plot_tree(dtc_hp.best_estimator_.named_steps['decisiontreeclassifier'],
max_depth=3, feature_names=tr.trf_cols,
proportion=True, filled=True, fontsize=8)
tr.update_recap('DTC', dtc_hp)
| accuracy | F2-score | |
|---|---|---|
| dummy | 85.17% | 8.18% |
| DTC | 85.48% | 13.66% |
La modification des hyperparamètres dégrade le F2-score. On peut mettre les variations sur le compte du fait que la méthode mise en place change l'échantillon à chaque fois.
params = {'logisticregression__C': np.logspace(-5, 5, 11),
}
lr_weighted = tr.best_estimator_random_search_cv(LogisticRegression(class_weight='balanced', max_iter=2000), params, X,y)
Fitting 5 folds for each of 10 candidates, totalling 50 fits
tr.update_recap('régression logistique', lr_weighted)
| accuracy | F2-score | |
|---|---|---|
| dummy | 85.17% | 8.18% |
| DTC | 85.48% | 13.66% |
| régression logistique | 67.69% | 39.12% |
lr_weighted.best_params_
{'logisticregression__C': 0.001}
plot_confusion_matrix(lr_weighted.best_estimator_, X,y)
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x7fd3d4d67d30>
Le F2-score est amélioré, alors que l'accuracy a baissé. Ce cas illustre bien le compromis à faire entre les métriques, et le fait que l'accuracy n'est pas forcément une bonne métrique dans le cas d'une distribution non équilibrée.
from sklearn.svm import LinearSVC
params = {'linearsvc__C':np.logspace(-3, 0, 4)}
linear_svc = tr.best_estimator_random_search_cv(LinearSVC(class_weight='balanced', max_iter=2000),
params, X, y)
/home/sophie/.local/lib/python3.8/site-packages/sklearn/model_selection/_search.py:285: UserWarning: The total space of parameters 4 is smaller than n_iter=10. Running 4 iterations. For exhaustive searches, use GridSearchCV.
Fitting 5 folds for each of 4 candidates, totalling 20 fits
tr.update_recap('SVC linéaire',linear_svc)
| accuracy | F2-score | |
|---|---|---|
| dummy | 85.17% | 8.18% |
| DTC | 85.48% | 13.66% |
| régression logistique | 67.69% | 39.12% |
| SVC linéaire | 67.04% | 38.03% |
plot_confusion_matrix(linear_svc.best_estimator_, X,y)
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x7fd37e02e8b0>
from sklearn.svm import SVC
params = [{'svc__kernel':['poly'], 'svc__degree':[2,3,4],'svc__C':np.logspace(-4,2,7), 'svc__gamma': np.logspace(-3,3,7)},
{'svc__kernel':['rbf'], 'svc__gamma': np.logspace(-3,3,7), 'svc__C':np.logspace(-4,2,7)},
{'svc__kernel': ['sigmoid'], 'svc__coef0': [0,1], 'svc__gamma': np.logspace(-3,3,7)}]
svc_k = tr.best_estimator_random_search_cv(SVC(class_weight='balanced',
cache_size=1000),
params, X, y)
Fitting 5 folds for each of 10 candidates, totalling 50 fits
svc_k.best_params_
{'svc__kernel': 'sigmoid', 'svc__gamma': 0.001, 'svc__coef0': 1}
tr.update_recap('SVC à noyau', svc_k)
| accuracy | F2-score | |
|---|---|---|
| dummy | 85.17% | 8.18% |
| DTC | 85.48% | 13.66% |
| régression logistique | 67.69% | 39.12% |
| SVC linéaire | 67.04% | 38.03% |
| SVC à noyau | 66.13% | 37.26% |
En raison du temps d'entraînement du modèle d'autoML, le modèle ne figure pas dans le présent notebook (l'ensemble des paramètres figure dans le notebook de travail).
On reporte ici le meilleur F2-score obtenu par l'autoML, qui est de 38,78%.
Le F2-score obtenu via l'autoML est du même ordre de grandeur que les meilleurs F2-scores obtenus sur les précédents modèles étudiés. On en conclut qu'on atteint les limites de l'optimisation du modèle (en l'état actuel des connaissances de l'auteur de ces recherches !).
On a également testé l'entraînement sur :
Il est donc acceptable de rester sur les données réduites, tant en nombre de variables qu'en nombre de données.
Récapitulons les performances des modèles étudiés, en nous attachant au F2-score :
tr.recap[['accuracy', 'F2-score']].sort_values(by=['F2-score'], ascending=False).style.format('{:.2%}')
| accuracy | F2-score | |
|---|---|---|
| régression logistique | 67.69% | 39.12% |
| SVC linéaire | 67.04% | 38.03% |
| SVC à noyau | 66.13% | 37.26% |
| DTC | 85.48% | 13.66% |
| dummy | 85.17% | 8.18% |
Les modèles offrant les meilleures performances au regard du F2-score sont la régression logistique et le SVM linéaire. Le recours à un modèle non linéaire simple (on n'a pas étudié les modèles neuronaux) ne semble pas améliorer les performances.
Comment choisir entre ces deux modèles ?
On relance le modèle retenu sur l'ensemble des données, pour plus de précision quant aux modalités les plus significatives des variables catégorielles.
params = {'logisticregression__C': np.logspace(-5, 5, 11),
}
lr_weighted = tr.best_estimator_random_search_cv(LogisticRegression(
class_weight='balanced', max_iter=2000), params, X,y, proportion=0.99)
Fitting 5 folds for each of 10 candidates, totalling 50 fits
# Construction du jeu d'étiquettes de données (permet de mieux lire les données qualitatives après one hot encoding)
quanti_labels = lr_weighted.best_estimator_.named_steps['columntransformer'].transformers_[0][2]
category_labels = lr_weighted.best_estimator_.named_steps['columntransformer'].transformers_[1][2]
ohe_labels = lr_weighted.best_estimator_.named_steps['columntransformer'].transformers_[1][1].categories_
quali_labels_dict = dict(zip(category_labels, ohe_labels))
quali_labels = []
for key in quali_labels_dict.keys():
for value in quali_labels_dict[key]:
quali_labels.append(f'{key} : {value}')
labels = quanti_labels + quali_labels
coefs_lr = pd.DataFrame(lr_weighted.best_estimator_.named_steps['logisticregression'].coef_.T, index=labels, columns=['coef'])
coefs_lr.sort_values(by=['coef'], key= lambda x:np.abs(x), ascending=False, inplace=True)
Coefficients les plus importants (jouant pour au moins 10% dans le total), classés par valeur absolue :
biggest_coefs = coefs_lr[np.abs(coefs_lr['coef'])>0.1]
biggest_coefs.index = [idx.replace(': ', ':<br>',1) for idx in biggest_coefs.index]
biggest_coefs.style.format('{:.2%}')
| coef | |
|---|---|
| NAME_EDUCATION_TYPE : Academic degree |
-70.03% |
| NAME_INCOME_TYPE : Unemployed |
63.03% |
| ORGANIZATION_TYPE : Realtor |
57.98% |
| NAME_INCOME_TYPE : Pensioner |
-52.99% |
| ORGANIZATION_TYPE : Transport: type 3 |
47.81% |
| ORGANIZATION_TYPE : Industry: type 12 |
-44.03% |
| ORGANIZATION_TYPE : Trade: type 4 |
-43.90% |
| EXT_SOURCE_2 | -39.08% |
| ORGANIZATION_TYPE : Legal Services |
38.99% |
| EXT_SOURCE_3 | -38.23% |
| NAME_INCOME_TYPE : Maternity leave |
37.39% |
| ORGANIZATION_TYPE : Military |
-33.88% |
| NAME_EDUCATION_TYPE : Lower secondary |
32.99% |
| ORGANIZATION_TYPE : Transport: type 1 |
-32.46% |
| NAME_INCOME_TYPE : Student |
-31.53% |
| ORGANIZATION_TYPE : Trade: type 2 |
-29.73% |
| ORGANIZATION_TYPE : Security Ministries |
-28.95% |
| ORGANIZATION_TYPE : Trade: type 6 |
-28.08% |
| ORGANIZATION_TYPE : Trade: type 3 |
25.85% |
| ORGANIZATION_TYPE : Industry: type 9 |
-25.15% |
| ORGANIZATION_TYPE : XNA |
24.66% |
| ORGANIZATION_TYPE : Construction |
24.49% |
| ORGANIZATION_TYPE : Bank |
-24.41% |
| NAME_EDUCATION_TYPE : Secondary / secondary special |
23.88% |
| ORGANIZATION_TYPE : Hotel |
-23.25% |
| ORGANIZATION_TYPE : Police |
-21.82% |
| OCCUPATION_TYPE : Low-skill Laborers |
21.28% |
| FLAG_OWN_CAR : Y |
-20.86% |
| FLAG_DOCUMENT_3 | 20.70% |
| ORGANIZATION_TYPE : Trade: type 5 |
-20.65% |
| ORGANIZATION_TYPE : Mobile |
20.45% |
| ORGANIZATION_TYPE : Advertising |
20.21% |
| OCCUPATION_TYPE : IT staff |
-19.97% |
| ORGANIZATION_TYPE : Industry: type 13 |
-19.61% |
| NAME_HOUSING_TYPE : Office apartment |
-19.49% |
| ORGANIZATION_TYPE : Industry: type 1 |
18.27% |
| ORGANIZATION_TYPE : Restaurant |
17.15% |
| OCCUPATION_TYPE : Accountants |
-16.77% |
| ORGANIZATION_TYPE : Telecom |
16.24% |
| EXT_SOURCE_1 | -16.17% |
| ORGANIZATION_TYPE : Industry: type 3 |
15.88% |
| CODE_GENDER : F |
-15.60% |
| ORGANIZATION_TYPE : Electricity |
-15.55% |
| YEARS_EMPLOYED | -15.33% |
| OCCUPATION_TYPE : Private service staff |
-14.61% |
| NAME_HOUSING_TYPE : Municipal apartment |
14.28% |
| CODE_GENDER : M |
14.22% |
| ORGANIZATION_TYPE : Postal |
13.76% |
| NAME_INCOME_TYPE : Businessman |
-13.33% |
| OCCUPATION_TYPE : Drivers |
12.71% |
| ORGANIZATION_TYPE : Self-employed |
12.19% |
| ORGANIZATION_TYPE : Industry: type 5 |
-11.83% |
| ORGANIZATION_TYPE : Religion |
-11.69% |
| ORGANIZATION_TYPE : Transport: type 4 |
11.67% |
| FLAG_DOCUMENT_6 | 11.50% |
| ORGANIZATION_TYPE : Industry: type 4 |
11.33% |
| FLAG_OWN_CAR : N |
11.05% |
| ORGANIZATION_TYPE : Business Entity Type 3 |
10.90% |
| AMT_REQ_CREDIT_BUREAU_YEAR | 10.89% |
| ORGANIZATION_TYPE : Industry: type 2 |
-10.41% |
| NAME_FAMILY_STATUS : Married |
-10.21% |
pos_biggest_coefs = biggest_coefs[biggest_coefs['coef']>0].head(20)
neg_biggest_coefs = biggest_coefs[biggest_coefs['coef']<0].head(20)
max_abs_coef = float(np.abs(biggest_coefs).max())
fig = px.bar(pos_biggest_coefs.sort_values(by='coef', ascending=True),
orientation='h',
x='coef',
width=800, height=800,
color=pos_biggest_coefs['coef'],
color_continuous_scale='ylorrd_r',
range_color=[0,1],
template="simple_white",
text='coef'
)
fig.update_traces(texttemplate='%{text:.2%}', textposition='inside',
textfont=dict(size=14)
)
fig.update_layout(coloraxis_showscale=False)
fig.update_xaxes(range=[0,max_abs_coef], title_text='Coefficients', tick0=0, dtick=0.1)
fig.update_yaxes(title_text='Variables', side='right', showline=False, ticks='')
py.iplot(fig)
fig = px.bar(neg_biggest_coefs.sort_values(by='coef', ascending=False),
x='coef',
orientation='h',
width=800, height=800,
color=neg_biggest_coefs['coef'],
color_continuous_scale='Tealgrn',
range_color=[-1, 0],
template="simple_white",
text='coef'
)
fig.update_traces(texttemplate='%{text:.2%}', textposition='inside',
textfont=dict(size=14)
)
fig.update_layout(coloraxis_showscale=False)
fig.update_xaxes(range=[-max_abs_coef,0], title_text='Coefficients', tick0=-max_abs_coef//10*10, dtick=0.1)
fig.update_yaxes(title_text='Variables', showline=False, ticks='')
py.iplot(fig)
On constate que la catégorie socio-professionnelle et le niveau d'étude sont les facteurs prépondérants de décision du modèle.
Le modèle souffre toutefois d'une sensibilité et d'une spécificités perfectibles :
y_pred = lr_weighted.predict(X)
tn, fp, fn, tp = confusion_matrix(y, y_pred).ravel()
sensitivity = tp / (tp + fn)
specificity = tn / (tn + fp)
print(f'La sensibilité est de {sensitivity:.2%} et la spécificité est de {specificity:.2%}.')
La sensibilité est de 66.48% et la spécificité est de 67.08%.